package session

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

/**
 * Session Provider
 * forexample:mem,redis,file...
 */
type SessionProvider interface {
	SessionInit(path string, gctime int) error
	SessionExist(id string) bool
	SessionRead(id string) (SessionInstance, error)
	SessionDestory(id string) error
	SessionGC()
}

/**
 * Session 操作实例接口
 */
type SessionInstance interface {
	Set(key, value interface{}) error
	Get(key interface{}) interface{}
	Delete(key interface{}) error
	SessionId() string
	SessionSave(w http.ResponseWriter)
	SessionClear() error
}

var providers = make(map[string]SessionProvider)

/**
 * [RegisterProvider reg a session provider]
 * @param {[string]} key      string          [provider name]
 * @param {[SessionProvider]} provider SessionProvider
 */
func RegisterProvider(key string, provider SessionProvider) {
	if provider == nil {
		panic("Provider is null.")
	}
	providers[key] = provider
}

type Config struct {
	CookieKey       string
	Domain          string
	GcLifeTime      int
	MaxLifeTime     int
	CookieLifeTime  int
	SessionIdLength int
}

type SessionFactory struct {
	provider SessionProvider
	config   *Config
}

func NewSessionFactory(providerKey string, cookiekey, domain string, providerconf string, lifetimeNlength ...int) (*SessionFactory, error) {
	provider, ok := providers[providerKey]
	if !ok {
		return nil, errors.New("Provider " + providerKey + " not found.")
	}
	conf := &Config{
		CookieKey: cookiekey,
		Domain:    domain,
	}
	if len(lifetimeNlength) > 0 && lifetimeNlength[0] > 0 {
		conf.GcLifeTime = lifetimeNlength[0]
	} else {
		conf.GcLifeTime = 3600
	}
	if len(lifetimeNlength) > 1 && lifetimeNlength[1] > 0 {
		conf.MaxLifeTime = lifetimeNlength[1]
	} else {
		conf.MaxLifeTime = conf.GcLifeTime
	}
	err := provider.SessionInit(providerconf, conf.MaxLifeTime)
	if err != nil {
		return nil, err
	}
	if len(lifetimeNlength) > 2 && lifetimeNlength[2] > 0 {
		conf.CookieLifeTime = lifetimeNlength[2]
	} else {
		conf.CookieLifeTime = conf.MaxLifeTime
	}
	if len(lifetimeNlength) > 3 && lifetimeNlength[3] > 0 {
		conf.SessionIdLength = lifetimeNlength[3]
	} else {
		conf.SessionIdLength = 16
	}
	return &SessionFactory{
		provider: provider,
		config:   conf,
	}, nil
}

func (this *SessionFactory) SessionStart(r *http.Request, w http.ResponseWriter) (session SessionInstance, errRtn error) {
	cookie, err := r.Cookie(this.config.CookieKey)
	if err != nil || cookie.Value == "" {
		sessionId, err := this.sessionId()
		if err != nil {
			session = nil
			errRtn = err
			return
		}
		session, errRtn = this.provider.SessionRead(sessionId)
		cookie := &http.Cookie{
			Name:     this.config.CookieKey,
			Value:    url.QueryEscape(sessionId),
			Path:     "/",
			HttpOnly: true,
			Domain:   this.config.Domain,
		}
		if this.config.CookieLifeTime > 0 {
			cookie.MaxAge = this.config.CookieLifeTime
			cookie.Expires = time.Now().Add(time.Duration(this.config.CookieLifeTime) * time.Second)
		}
		http.SetCookie(w, cookie)
		r.AddCookie(cookie)
	} else {
		sessionId, err := url.QueryUnescape(cookie.Value)
		if err != nil {
			session = nil
			errRtn = err
			return
		}
		if this.provider.SessionExist(sessionId) {
			session, err = this.provider.SessionRead(sessionId)
		} else {
			sessionId, err := this.sessionId()
			if err != nil {
				session = nil
				errRtn = err
				return
			}
			session, errRtn = this.provider.SessionRead(sessionId)
			cookie := &http.Cookie{
				Name:     this.config.CookieKey,
				Value:    url.QueryEscape(sessionId),
				Path:     "/",
				HttpOnly: true,
				Domain:   this.config.Domain,
			}
			if this.config.CookieLifeTime > 0 {
				cookie.MaxAge = this.config.CookieLifeTime
				cookie.Expires = time.Now().Add(time.Duration(this.config.CookieLifeTime) * time.Second)
			}
			http.SetCookie(w, cookie)
			r.AddCookie(cookie)
		}
	}
	return
}

func (this *SessionFactory) GetSessionInstance(id string) (SessionInstance, error) {
	return this.provider.SessionRead(id)
}

func (this *SessionFactory) SessionDestory(r *http.Request, w http.ResponseWriter) {
	cookie, err := r.Cookie(this.config.CookieKey)
	if err != nil || cookie.Value == "" {
		return
	}
	this.provider.SessionDestory(cookie.Value)
	cookie = &http.Cookie{
		Name:     this.config.CookieKey,
		Path:     "/",
		HttpOnly: true,
		Expires:  time.Now(),
		MaxAge:   -1,
	}
	http.SetCookie(w, cookie)
}

func (this *SessionFactory) sessionId() (string, error) {
	buff := make([]byte, this.config.SessionIdLength)
	n, err := rand.Read(buff)
	if err != nil || n != len(buff) {
		return "", errors.New("New session id failed.")
	}
	fmt.Println("hex:", hex.EncodeToString(buff), this.config.SessionIdLength)
	return hex.EncodeToString(buff), nil
}

func (this *SessionFactory) GC() {
	this.provider.SessionGC()
	time.AfterFunc(time.Duration(this.config.GcLifeTime)*time.Second, func() { this.GC() })
}
